﻿using System;
using System.Collections.Generic;
using System.Data.Linq;
using System.Linq;
using System.Reflection;
using System.Threading;
using SBToolkit.Core;

namespace SBAudit.LinqToSql
{
    public static class AuditExtensions
    {
        /// <summary>
        /// Audits all changes in the provided data context and then submits all pending changes.
        /// </summary>
        /// <param name="context">Data context to audit changes in.</param>
        /// <param name="failureMode">Conflict failure mode.</param>
        /// <param name="baseSubmit">Original submit method of the specified data context.</param>
        /// <param name="transformation">The transformation delegate, used to transform the list of <see cref="AuditEntry"/> 
        /// to the specified database type <typeparamref name="TEntity"/>.</param>
        /// <typeparam name="TEntity">The type of audit entity to store in database.</typeparam>
        /// <returns>A collection of all audited items.</returns>
        public static void AuditAndSubmitChanges<TEntity>(this DataContext context, Action<ConflictMode> baseSubmit,
            ConflictMode failureMode, Func<IEnumerable<AuditEntry>, IEnumerable<TEntity>> transformation)
            where TEntity : class
        {
            // store context change set before submit
            var insertedItems = context.GetChangeSet().Inserts;
            var updatedItems = context.GetChangeSet().Updates;
            var deletedItems = context.GetChangeSet().Deletes;

            var auditEntries = new List<AuditEntry>();
            // Audit for the update operation must be called before submit while there is a way to get modified members
            auditEntries.AddRange(Audit(context, updatedItems, AuditAction.Update));
            auditEntries.AddRange(Audit(context, deletedItems, AuditAction.Delete));

            // submit changes so will have entities with actual keys
            baseSubmit(failureMode);

            auditEntries.AddRange(Audit(context, insertedItems, AuditAction.Insert));

            context.GetTable<TEntity>().InsertAllOnSubmit(transformation(auditEntries));

            // ignore any warnings for the audit records
            baseSubmit(ConflictMode.ContinueOnConflict);
        }

        /// <summary>
        /// Audits specified entities from the provided data context.
        /// </summary>
        /// <param name="context">Data context to audit changes in.</param>
        /// <param name="entities">The entities to audit.</param>
        /// <param name="action">Audit action.</param>
        /// <returns>A collection of audited items for the specified audit operation.</returns>
        public static IEnumerable<AuditEntry> Audit(this DataContext context, IEnumerable<object> entities, AuditAction action)
        {
            entities = entities.Where(o => !o.IsAttributedAs<IgnoreAuditAttribute>()).ToList();
            foreach (var entity in entities)
            {
                var entityType = entity.GetType();
                var table = context.GetTable(entityType);
                var metaTable = context.Mapping.GetTable(entityType);
                var key = (PropertyInfo)metaTable.RowType.DataMembers.Single(md => md.IsPrimaryKey).Member;

                var auditEntry = new AuditEntry
                {
                    Action = action,
                    EntityTable = metaTable.TableName,
                    EntityTableKey = key.GetValue(entity, null),
                    AuditDate = SystemDateTime.Now(),
                    UserName = Thread.CurrentPrincipal.Identity != null ? Thread.CurrentPrincipal.Identity.Name : Environment.UserName
                };

                IEnumerable<AuditField> fields;

                if (action == AuditAction.Insert)
                {
                    fields = metaTable.RowType.DataMembers.
                        Where(dm => dm.Association == null).
                        Select(dm => dm.Member).
                        Cast<PropertyInfo>().
                        Select(dm => new AuditField
                        {
                            FieldName = dm.Name,
                            OldValue = null,
                            NewValue = dm.GetValue(entity, null).ToNullableString()
                        }).ToList();
                }
                else if (action == AuditAction.Delete)
                {
                    fields = metaTable.RowType.DataMembers.
                        Where(dm => dm.Association == null).
                        Select(dm => dm.Member).
                        Cast<PropertyInfo>().
                        Select(m => new AuditField
                        {
                            FieldName = m.Name,
                            OldValue = m.GetValue(entity, null).ToNullableString(),
                            NewValue = null
                        }).ToList();
                }
                else
                {
                    fields = table.GetModifiedMembers(entity).
                        Select(mi => new AuditField
                        {
                            FieldName = mi.Member.Name,
                            OldValue = mi.OriginalValue.ToNullableString(),
                            NewValue = mi.CurrentValue.ToNullableString()
                        });
                }

                foreach (var field in fields)
                {
                    var auditField = new AuditField
                    {
                        FieldName = field.FieldName,
                        OldValue = field.OldValue,
                        NewValue = field.NewValue
                    };
                    auditEntry.Fields.Add(auditField);
                }

                yield return auditEntry;
            }
        }

        /// <summary>
        /// Converts specified object to the string representation.
        /// </summary>
        /// <param name="obj">The object to convert.</param>
        /// <returns>String representation of the object or null.</returns>
        private static string ToNullableString(this object obj)
        {
            return obj == null ? null : obj.ToString();
        }
    }
}
